Allowing stateful service loaders - #1616
Conversation
There was a problem hiding this comment.
Pull request overview
This PR changes how service-provider implementations are resolved, aiming to avoid caching provider instances while still avoiding repeated classpath scanning by caching ServiceLoader objects and re-streaming them per invocation.
Changes:
- Replaced caching of loaded service instances with caching of
ServiceLoader<?>inWorkflowApplication. - Updated
serviceLoadedClasses(...)/serviceLoadedClass(...)to re-stream providers on each call. - Updated
DefaultTaskExecutorFactoryto resolveCallableTaskBuilderimplementations viaWorkflowApplicationinstead of its own cachedServiceLoaderresult.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowApplication.java | Switches service resolution cache from instance lists to cached ServiceLoader and re-streaming per call. |
| impl/core/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java | Routes CallableTaskBuilder discovery through WorkflowApplication service-loading helpers. |
Suppressed comments (1)
impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowApplication.java:724
serviceLoadedClassnow returns the first provider inServiceLoaderiteration order, which is not guaranteed to respectServicePriority/Comparableordering. This can cause a lower-priority implementation to be selected.
ServiceLoader<?> serviceLoader =
servicesLoaded.computeIfAbsent(serviceClass, ServiceLoader::load);
return (T)
serviceLoader.stream()
.map(ServiceLoader.Provider::get)
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Service loaders might be stateful, so caching them might be an isssue. Since internally serviceloader class already caches the classpath search, the right solution is to store a reference to the ServiceLoader itself and call stream for every invocation. That way, we have the best of both world, fresh new instance of the service loader class for every invocation (supporting stateful) and avoid the classpath search for every invocation (the original performance issue to be fixed) Signed-off-by: Francisco Javier Tirado Sarti <ftirados@ibm.com>
f0ff8be to
4bc755e
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
Suppressed comments (2)
impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowApplication.java:716
ServiceLoaderinstances are not documented as thread-safe. Caching a singleServiceLoaderinservicesLoadedand then callingstream()on it from multiple threads can lead to racy provider discovery/iteration. Consider synchronizing access per cached loader (or otherwise ensuring single-threaded use) and tighten the generics to avoid the wildcard/uncheckedListcast.
public <T extends Comparable<?>> List<T> serviceLoadedClasses(Class<T> clazz) {
ServiceLoader<?> serviceLoader = servicesLoaded.computeIfAbsent(clazz, ServiceLoader::load);
return (List<T>) serviceLoader.stream().map(ServiceLoader.Provider::get).sorted().toList();
impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowApplication.java:727
- Using
sorted().findFirst()forces a full sort (O(n log n)) even though only the minimum element is needed. Prefermin(Comparator.naturalOrder())to select the highest-priority implementation without sorting the entire stream (and keep access synchronized if the cachedServiceLoadercan be used concurrently).
serviceLoader.stream()
.map(ServiceLoader.Provider::get)
.sorted()
.findFirst()
.orElseThrow(
Service loaders might be stateful, so caching them might be an isssue. Since internally serviceloader class already caches the classpath search, the right solution is to store a reference to the ServiceLoader itself and call stream for every invocation.
That way, we have the best of both world, fresh new instance of the service loader class for every invocation (supporting stateful) and avoid the classpath search for every invocation (the original performance issue to be fixed)